'use client'; import { useRouter } from 'next/navigation'; import { useActionState, useEffect, useState } from 'react'; import { FoxButton } from '@fox/core/components/fox-button'; import { FoxButtonGroup } from '@fox/core/components/fox-button-group'; import { FoxButtonPanel } from '@fox/core/components/fox-button-panel'; import { FoxCard } from '@fox/core/components/fox-card'; import { FoxDescriptionList } from '@fox/core/components/fox-description-list'; import { FoxHelperText } from '@fox/core/components/fox-helper-text'; import { FoxInput } from '@fox/core/components/fox-input'; import { FoxPageHeader } from '@fox/core/components/fox-page-header'; import { FoxSelect } from '@fox/core/components/fox-select'; import { submitFormAction } from '@/app/_hooks/submit-form-action'; import { useBlockingAction } from '@/app/_hooks/use-blocking-action'; import { useFeedback } from '@/app/_hooks/use-feedback'; import type { CommonCode } from '@/lib/domain/common-code'; import { MERCHANT_DETAIL_VISIBILITY_OPTIONS, MERCHANT_VOUCHER_USE_OPTIONS, formatMerchantDetailStatusLabel, formatOptionalValue, type MerchantDetail, type MerchantDetailSummary, } from '@/lib/domain/merchant-detail'; import { CLOSED_DAYS_MAX_LENGTH, DESCRIPTION_MAX_LENGTH, INITIAL_MERCHANT_DETAIL_FORM_STATE, SORT_ORDER_MAX, } from '@/lib/domain/merchant-detail-form'; import { MERCHANT_DETAILS_PATH, merchantDetailPath } from '@/lib/domain/merchant-detail-query'; import type { MerchantProduct } from '@/lib/domain/merchant-product'; import { formatPhoneNumber } from '@/lib/domain/phone-number'; import { deleteMerchantDetailAction, saveMerchantDetailAction, updateMerchantDetailVisibilityAction, } from '../../_actions'; import { MerchantChoiceChips } from '../../_components/merchant-choice-chips'; import { MerchantImageField } from '../../_components/merchant-image-field'; import { MerchantProductTable } from '../../_components/merchant-product-table'; import styles from './merchant-detail-form.module.scss'; interface MerchantDetailFormProps { /** 이 화면의 가맹점 — 업체명·연락처·주소의 출처. 상세 유무와 무관하게 늘 정해져 있다. */ merchant: MerchantDetailSummary; /** 상세가 이미 있으면 그 값. 없으면 이 화면에서 처음 만든다. */ detail?: MerchantDetail; products?: MerchantProduct[]; categories: CommonCode[]; } /** * 가맹몰 상세 — 기획 BO-MAL-002(등록)·003(수정)을 한 화면으로 합쳤다(사용자 확정). 목록의 행에서 * 들어오며 `detail`이 있으면 고치고, 없으면 처음 만든다. 첫 저장 뒤에도 화면 구성은 그대로라 * 바로 메뉴를 붙일 수 있고, 저장은 그 자리에 머문다. * * 업체명·연락처·우편번호·주소는 가맹점 원본이라 늘 읽기 전용이다(사용자 확정). 기획의 [주소 검색]은 * 가맹점 관리 화면의 것이라 여기서는 값을 바꾸지 않는다. */ export function MerchantDetailForm({ merchant, detail, products = [], categories, }: MerchantDetailFormProps) { const router = useRouter(); const { showAlert, hideAlert, showToast } = useFeedback(); const [isBusy, startAction] = useBlockingAction(); const [state, formAction, isPending] = useActionState( saveMerchantDetailAction, INITIAL_MERCHANT_DETAIL_FORM_STATE ); const isEdit = detail !== undefined; const errors = state.status === 'error' ? (state.errors ?? {}) : {}; const frcsNo = merchant.id; const [categoryCode, setCategoryCode] = useState(detail?.categoryCode ?? ''); const [visibility, setVisibility] = useState([ detail?.isVisible === false ? 'N' : 'Y', ]); const [voucherUse, setVoucherUse] = useState(['Y']); useEffect(() => { if (state.status !== 'success') return; if (state.created) { showToast({ variant: 'success', message: '등록했습니다. 이어서 메뉴를 추가할 수 있습니다.' }); router.replace(merchantDetailPath(state.frcsNo)); } else { showToast({ variant: 'success', message: '저장했습니다.' }); router.refresh(); } }, [state, router, showToast]); function toggleVisibility() { if (!detail) return; const next = detail.isVisible === false; startAction(async () => { const result = await updateMerchantDetailVisibilityAction(detail.id, next); showToast( result.status === 'error' ? { variant: 'danger', message: result.message } : { variant: 'success', message: next ? '노출로 바꿨습니다.' : '비활성으로 바꿨습니다.' } ); }); } function runDelete() { if (!detail) return; hideAlert(); startAction(async () => { const result = await deleteMerchantDetailAction(detail.id); if (result.status === 'error') { showToast({ variant: 'danger', message: result.message }); return; } showToast({ variant: 'success', message: '삭제했습니다.' }); router.push(MERCHANT_DETAILS_PATH); }); } function confirmDelete() { if (!detail) return; showAlert({ variant: 'danger', title: '가맹몰 상세를 삭제하시겠습니까?', message: `"${detail.name}"의 상세 정보를 삭제합니다. 삭제 후에는 되돌릴 수 없습니다.`, actions: ( ), }); } return ( <>
submitFormAction(event, formAction)} className={styles.form} >
({ value: code.code, label: code.label }))} value={categoryCode} onValueChange={setCategoryCode} error={Boolean(errors.categoryCode)} /> {errors.categoryCode && }
{/* 저장으로 파일 id가 바뀌면 다시 마운트해 칸 상태를 서버 값으로 되돌린다. */} {detail && ( Boolean(value)) .join(' / ') || '-', }, { key: 'status', term: '현재 상태', description: formatMerchantDetailStatusLabel(detail) }, ]} /> )} {state.status === 'error' && state.message && ( )} {detail && ( <> )} router.push(MERCHANT_DETAILS_PATH)} /> ); }